<html>
 <head>
  <meta charset="UTF-8">
 </head>
 <body>
  <h1 data-lake-id="suI63" id="suI63"><span data-lake-id="u02995b86" id="u02995b86">典型回答</span></h1>
  <p data-lake-id="u7004b74e" id="u7004b74e"><br></p>
  <p data-lake-id="ue360b871" id="ue360b871"><span data-lake-id="u30e35702" id="u30e35702">所谓"超卖"指的就是商品卖多了，一般我们在商品扣减库存的时候，都会先判断库存够不够，如果够在进行扣减，不够则直接返回下单失败。</span></p>
  <p data-lake-id="ua7277b87" id="ua7277b87"><span data-lake-id="u4f5a67c0" id="u4f5a67c0">​</span><br></p>
  <p data-lake-id="ud67827c5" id="ud67827c5"><span data-lake-id="uc586331a" id="uc586331a">但是，如果在高并发场景中，可能存在以下情况：</span></p>
  <p data-lake-id="u2d79600c" id="u2d79600c"><span data-lake-id="ue742e97d" id="ue742e97d">​</span><br></p>
  <p data-lake-id="u646edbf3" id="u646edbf3"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1676625421967-7b84f672-5865-4032-923a-34415e18f55b.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_47%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uae9d59ad" id="uae9d59ad"><br></p>
  <p data-lake-id="u740f25ab" id="u740f25ab"><span data-lake-id="u042c2e61" id="u042c2e61">当有两个并发线程，同时查询库存，这时数据库中库存剩余1，所以两个线程都得到1的库存，然后经过库存校验之后分别开始进行库存扣减，最终导致库存被扣减成负数。</span></p>
  <p data-lake-id="uef9338b3" id="uef9338b3"><span data-lake-id="u0824c4d5" id="u0824c4d5">​</span><br></p>
  <p data-lake-id="uf4abda22" id="uf4abda22"><span data-lake-id="u04e14eb5" id="u04e14eb5">以上，就是一个典型的高并发情况下的超卖问题。</span></p>
  <p data-lake-id="u69929cec" id="u69929cec"><span data-lake-id="u2e4c8392" id="u2e4c8392">​</span><br></p>
  <p data-lake-id="u10e0666c" id="u10e0666c"><span data-lake-id="u07052ef9" id="u07052ef9">之所以会发生以上问题，主要是因为并发导致的，所以，解决超卖的问题本质上是解决并发问题。以上问题，</span><strong><span data-lake-id="u391a1702" id="u391a1702">最终就是要实现库存扣减过程中的原子性和有序性。</span></strong></p>
  <p data-lake-id="ue937819a" id="ue937819a"><span data-lake-id="u0f652e6e" id="u0f652e6e">​</span><br></p>
  <p data-lake-id="ucd643e2c" id="ucd643e2c"><strong><span data-lake-id="u1edfd6e2" id="u1edfd6e2">原子性</span></strong><span data-lake-id="u33d6286a" id="u33d6286a">：库存查询、库存判断以及库存扣减动作，作为一个原子操作，过程中不会被打断，也不会有其他线程执行。</span></p>
  <p data-lake-id="u26b4170e" id="u26b4170e"><strong><span data-lake-id="u365cabdf" id="u365cabdf">有序性</span></strong><span data-lake-id="u11e7e99c" id="u11e7e99c">：多个并发操作需要排队执行。</span></p>
  <p data-lake-id="ua9e9d73e" id="ua9e9d73e"><br></p>
  <h2 data-lake-id="oHoaI" id="oHoaI"><span data-lake-id="ua665ac16" id="ua665ac16">数据库扣减</span></h2>
  <p data-lake-id="u732ee16d" id="u732ee16d"><br></p>
  <p data-lake-id="u39b746da" id="u39b746da"><span data-lake-id="u5d1d612d" id="u5d1d612d">数据库中进行库存扣减是最容易想到的方案，这个方案实现起来非常简单。</span></p>
  <p data-lake-id="u65d69ed5" id="u65d69ed5"><span data-lake-id="u024595f1" id="u024595f1">​</span><br></p>
  <p data-lake-id="uab3e36f4" id="uab3e36f4"><span data-lake-id="ua4a93e65" id="ua4a93e65">在扣减过程中，想要保证原子性和有序性，我们可以采用加锁的方式，无论是悲观锁、还是乐观锁都可以实现的。</span></p>
  <p data-lake-id="u84483809" id="u84483809"><span data-lake-id="u9856e05a" id="u9856e05a">​</span><br></p>
  <p data-lake-id="u7da163ea" id="u7da163ea"><span data-lake-id="u896bae77" id="u896bae77">但是，如果使用悲观锁来实现的话，就会导致很多请求被迫阻塞并且排队，那么如果并发请求量很大的话，就可能直接把数据库给拖垮了。</span></p>
  <p data-lake-id="uacc6b687" id="uacc6b687"><span data-lake-id="u4c0ed673" id="u4c0ed673">​</span><br></p>
  <p data-lake-id="u4eab139f" id="u4eab139f"><span data-lake-id="u77dafcb8" id="u77dafcb8">如果是乐观锁的话，可以用版本号的方式来控制有序执行，但是这个问题在于高并发场景中会存在大量的失败，而且高并发场景中也不适合使用乐观锁，因为乐观锁在update的过程中也是需要加行级锁的，也是会出现阻塞的情况。</span></p>
  <p data-lake-id="ub72138e6" id="ub72138e6"><span data-lake-id="u093c25db" id="u093c25db">​</span><br></p>
  <p data-lake-id="u52ccc1ef" id="u52ccc1ef"><span data-lake-id="u420fa6da" id="u420fa6da">那么，在库存扣减时，如果不加锁可以吗？</span></p>
  <p data-lake-id="ud65c5293" id="ud65c5293"><span data-lake-id="ub0457b08" id="ub0457b08">​</span><br></p>
  <p data-lake-id="uad421240" id="uad421240"><span data-lake-id="ude2a0e7f" id="ude2a0e7f">其实是可以的，我们就借助数据库自己执行引擎的顺序执行机制，只要保证库存不要扣减成负数就行了，那么可行的方案是通过SQL语句就能控制，如：</span></p>
  <p data-lake-id="ufb09f187" id="ufb09f187"><span data-lake-id="u9808d025" id="u9808d025">​</span><br></p>
  <pre lang="java"><code>
update inventory 
set quantity = quantity - #{count} 
where sku_id='123' and quantity &gt;= #{count} 
</code></pre>
  <p data-lake-id="u9ed53e97" id="u9ed53e97"><span data-lake-id="ud773fe8f" id="ud773fe8f">​</span><br></p>
  <p data-lake-id="ue8a6c5f9" id="ue8a6c5f9"><span data-lake-id="u480a0c07" id="u480a0c07">也就是说，如果上述SQL可以执行成功的话，是可以确保库存余量大于等于0的，这就避免了超卖的发生。</span></p>
  <p data-lake-id="uef2a7b55" id="uef2a7b55"><span data-lake-id="u04982353" id="u04982353">​</span><br></p>
  <p data-lake-id="u7d67b5d8" id="u7d67b5d8"><span data-lake-id="u49084c90" id="u49084c90">但是这个方案好吗？其实是不好的。</span></p>
  <p data-lake-id="ubaa21d14" id="ubaa21d14"><span data-lake-id="uaf3fb58e" id="uaf3fb58e">​</span><br></p>
  <p data-lake-id="u4287db42" id="u4287db42"><span data-lake-id="ubc06512a" id="ubc06512a">因为这个方案本质上和乐观锁的方案缺点是一样的，都是完全依赖数据库，并且高并发情况下，多个线程同时update inventory 的时候会发生阻塞，不仅会很慢，还会把数据库拖垮的。详见：</span></p>
  <p data-lake-id="u4112390a" id="u4112390a"><span data-lake-id="uaa6d70e8" id="uaa6d70e8">​</span><br></p>
  <p data-lake-id="u922e75de" id="u922e75de"><br></p>
  <p data-lake-id="u3a28173e" id="u3a28173e"><span data-lake-id="u72e9c36e" id="u72e9c36e">正常来说，MySQL的热点行更新最多也就抗200-300的并发更新，如果想要抗的更多，要么就是提升硬件水平，要么就是做一些技术改造，比如inventory hint的方式。</span></p>
  <p data-lake-id="u4e6f284d" id="u4e6f284d"><span data-lake-id="u110651f7" id="u110651f7">​</span><br></p>
  <p data-lake-id="uf9e08119" id="uf9e08119"><br></p>
  <p data-lake-id="u6ee723dd" id="u6ee723dd"><span data-lake-id="u1059d133" id="u1059d133">​</span><br></p>
  <p data-lake-id="u958ee1e0" id="u958ee1e0"><span data-lake-id="uccb6b3b3" id="uccb6b3b3">那么，不用数据库扣减的话，可以用什么呢？答案是可以借助缓存的扣减。</span></p>
  <p data-lake-id="ua7d72c80" id="ua7d72c80"><br></p>
  <h2 data-lake-id="tcACm" id="tcACm"><span data-lake-id="u17b6b60f" id="u17b6b60f">Redis扣减</span></h2>
  <p data-lake-id="ued009a5b" id="ued009a5b"><br></p>
  <p data-lake-id="u1638b2d6" id="u1638b2d6"><span data-lake-id="u71e3ee80" id="u71e3ee80">我们可以基于Redis做库存扣减的，借助Redis的单线程执行的特性，再加上Lua脚本执行过程中的原子性保障，我们可以在Redis中通过Lua脚本进行库存扣减。</span></p>
  <p data-lake-id="u62794523" id="u62794523"><span data-lake-id="u631f10ba" id="u631f10ba">​</span><br></p>
  <p data-lake-id="u74328472" id="u74328472"><span data-lake-id="ufa6d8bac" id="ufa6d8bac">在Redis中，使用以下Lua脚本：</span></p>
  <p data-lake-id="u03068e80" id="u03068e80"><br></p>
  <pre lang="java"><code>
local key = KEYS[1] -- 商品的键名
local amount = tonumber(ARGV[1]) -- 扣减的数量

-- 获取商品当前的库存量
local stock = tonumber(redis.call('get', key))

-- 如果库存足够，则减少库存并返回新的库存量
if stock &gt;= amount then
    redis.call('decrby', key, amount)
    return redis.call('get', key)
else
    return "INSUFFICIENT STOCK"
end

</code></pre>
  <p data-lake-id="ud314d1d0" id="ud314d1d0"><br></p>
  <p data-lake-id="u090dccea" id="u090dccea"><span data-lake-id="u1981a17b" id="u1981a17b">先从Redis中取出当前的剩余库存，然后判断是否足够扣减，如果足够的话，就进行扣减，否则就返回库存不足。</span></p>
  <p data-lake-id="u8d86558b" id="u8d86558b"><span data-lake-id="ua0f59284" id="ua0f59284">​</span><br></p>
  <p data-lake-id="uf3cd7fe3" id="uf3cd7fe3"><span data-lake-id="u57a0af0d" id="u57a0af0d">因为lua脚本在执行过程中，可以避免被打断，并且redis执行的过程也是单线程的，所以在脚本中进行判断，再扣减，这个过程是可以避免并发的。所以也就可以实现前面我们说的原子性+有序性了。</span></p>
  <p data-lake-id="u4c88b922" id="u4c88b922"><span data-lake-id="u547c5873" id="u547c5873">​</span><br></p>
  <p data-lake-id="ubc642604" id="ubc642604"><span data-lake-id="u061344c2" id="u061344c2">并且Redis是一个高性能的分布式缓存，使用Lua脚本扣减库存的方案也非常的高效。</span></p>
  <p data-lake-id="u2e82fa84" id="u2e82fa84"><span data-lake-id="ueeb0f499" id="ueeb0f499">​</span><br></p>
  <h1 data-lake-id="YK26l" id="YK26l"><span data-lake-id="u0e704796" id="u0e704796">扩展知识</span></h1>
  <p data-lake-id="u5833d615" id="u5833d615"><br></p>
  <h2 data-lake-id="MjMG4" id="MjMG4"><span data-lake-id="u30134648" id="u30134648">一致性保证</span></h2>
  <p data-lake-id="u052c873b" id="u052c873b"><br></p>
  <p data-lake-id="u56ee2211" id="u56ee2211"><span data-lake-id="u45207883" id="u45207883">上面我们提到了两个方案，一个是通过数据库进行库存扣减，一个是通过redis实现扣减，一般，在实际应用过程中，这两种方案会结合使用。</span></p>
  <p data-lake-id="u06d64a32" id="u06d64a32"><span data-lake-id="u8f45d100" id="u8f45d100">​</span><br></p>
  <p data-lake-id="ude9ba6a6" id="ude9ba6a6"><span data-lake-id="ub61479d9" id="ub61479d9">也就是说先在Redis中做扣减，利用Redis来抗高并发流量，然后再同步到数据库中，在数据库中做扣减并进行持久化存储，避免Redis挂了导致数据丢失。</span></p>
  <p data-lake-id="uc75c3c4b" id="uc75c3c4b"><span data-lake-id="uabbfe1a9" id="uabbfe1a9">​</span><br></p>
  <p data-lake-id="u5457469b" id="u5457469b"><span data-lake-id="u46d1f314" id="u46d1f314">一般的做法是，先在Redis中做扣减，然后发送一个MQ消息，消费者在接到消息之后做数据库中库存的真正扣减及业务逻辑操作。</span></p>
  <p data-lake-id="u771f45f2" id="u771f45f2"><span data-lake-id="uef036462" id="uef036462">​</span><br></p>
  <p data-lake-id="u83a4d416" id="u83a4d416"><img src="https://cdn.nlark.com/yuque/0/2023/png/5378072/1680415404103-a88ac6c0-7538-4c31-8f0f-9219e3a0fc0b.png?x-oss-process=image%2Fwatermark%2Ctype_d3F5LW1pY3JvaGVp%2Csize_46%2Ctext_SmF2YSA4IEd1IFA%3D%2Ccolor_FFFFFF%2Cshadow_50%2Ct_80%2Cg_se%2Cx_10%2Cy_10"></p>
  <p data-lake-id="uf45d00b3" id="uf45d00b3"><br></p>
  <p data-lake-id="u318b7b92" id="u318b7b92"><span data-lake-id="ue5e53ad5" id="ue5e53ad5">这样做，我们可以保证Redis中的数据和数据库中的数据的一个最终一致性。并且也能避免超卖的发生。</span></p>
  <p data-lake-id="u79152777" id="u79152777"><span data-lake-id="u275ad883" id="u275ad883">​</span><br></p>
  <p data-lake-id="u791a9313" id="u791a9313"><span data-lake-id="uc1385bb9" id="uc1385bb9">但是，这个方案有个问题，就是可能导致少卖</span></p>
  <h2 data-lake-id="mRCCI" id="mRCCI"><span data-lake-id="u78cf0248" id="u78cf0248">少卖</span></h2>
  <p data-lake-id="ud587e240" id="ud587e240"><br></p>
  <p data-lake-id="u50bac41a" id="u50bac41a"><span data-lake-id="u3327d96d" id="u3327d96d">假设，上面的流程中，第1步执行成功了，Redis中库存成功扣减了，但是后续第2步的消息没有发出去，或者后面的消费过程中消息丢了或者失败了等情况。</span></p>
  <p data-lake-id="u5115461d" id="u5115461d"><span data-lake-id="u2a061802" id="u2a061802">​</span><br></p>
  <p data-lake-id="u03eceaa5" id="u03eceaa5"><span data-lake-id="ud2fae101" id="ud2fae101">就会导致Redis中的库存被扣减了，但是数据库库存没扣减，业务的实际操作没发生。这时候的结果就是Redis中发生了多扣，那么带来的业务问题就是少卖。</span></p>
  <p data-lake-id="u476268f2" id="u476268f2"><span data-lake-id="u237822d0" id="u237822d0">​</span><br></p>
  <p data-lake-id="u86d7d596" id="u86d7d596"><span data-lake-id="u3ec33e0a" id="u3ec33e0a">那么，想要解决这类问题呢，就需要引入一些对账的机制，做一些准实时的核对，针对这类情况及时发现，如果少卖很多的话，那么就需要再把这些库存加回去。</span></p>
  <p data-lake-id="ud26c1eba" id="ud26c1eba"><span data-lake-id="ua564d86f" id="ua564d86f">​</span><br></p>
  <p data-lake-id="uc1b3e8a1" id="uc1b3e8a1"><span data-lake-id="u3d82417f" id="u3d82417f">一般在很多成熟的电商公司中，不管前面的方案做的多么完善，这个核对系统都是必不可少的。及时的核对出超卖、少卖等问题。</span></p>
 </body>
</html>